// Copyright (c) 2021, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// This imports 'codes/cfe_codes.dart' instead of 'api_prototype/codes.dart' to
// avoid cyclic dependency between `package:vm/modular` and `package:front_end`.
import 'package:front_end/src/codes/cfe_codes.dart'
    show
        messageFfiDefaultAssetDuplicate,
        messageFfiNativeDuplicateAnnotations,
        messageFfiNativeFieldMissingType,
        messageFfiNativeFieldMustBeStatic,
        messageFfiNativeFieldType,
        messageFfiNativeFunctionMissingType,
        messageFfiNativeMustBeExternal,
        messageFfiNativeOnlyNativeFieldWrapperClassCanBePointer,
        templateCantHaveNamedParameters,
        templateCantHaveOptionalParameters,
        templateFfiNativeUnexpectedNumberOfParameters,
        templateFfiNativeUnexpectedNumberOfParametersWithReceiver,
        templateFfiTypeInvalid;
import 'package:kernel/ast.dart';
import 'package:kernel/class_hierarchy.dart' show ClassHierarchy;
import 'package:kernel/core_types.dart';
import 'package:kernel/library_index.dart' show LibraryIndex;
import 'package:kernel/reference_from_index.dart' show ReferenceFromIndex;
import 'package:kernel/target/targets.dart' show DiagnosticReporter;
import 'package:kernel/type_environment.dart';

import 'common.dart'
    show FfiStaticTypeError, FfiTransformer, NativeType, FfiTypeCheckDirection;
import 'native_type_cfe.dart';

/// Transform @Native annotated functions into FFI native function pointer
/// functions.
void transformLibraries(
  Component component,
  CoreTypes coreTypes,
  ClassHierarchy hierarchy,
  List<Library> libraries,
  DiagnosticReporter diagnosticReporter,
  ReferenceFromIndex? referenceFromIndex,
) {
  final index = LibraryIndex(component, [
    'dart:ffi',
    'dart:_internal',
    'dart:typed_data',
    'dart:nativewrappers',
    'dart:isolate',
  ]);
  // Skip if dart:ffi isn't loaded (e.g. during incremental compile).
  if (index.tryGetClass('dart:ffi', 'Native') == null) {
    return;
  }
  final transformer = FfiNativeTransformer(
    index,
    coreTypes,
    hierarchy,
    diagnosticReporter,
    referenceFromIndex,
  );
  libraries.forEach(transformer.visitLibrary);
}

class FfiNativeTransformer extends FfiTransformer {
  final DiagnosticReporter diagnosticReporter;
  final ReferenceFromIndex? referenceFromIndex;
  final Class assetClass;
  final Class nativeClass;
  final Class nativeFunctionClass;
  final Field assetAssetField;
  final Field nativeSymbolField;
  final Field nativeAssetField;
  final Field resolverField;

  StringConstant? currentAsset;

  // VariableDeclaration names can be null or empty string, in which case
  // they're automatically assigned a "temporary" name like `#t0`.
  static const variableDeclarationTemporaryName = null;

  FfiNativeTransformer(
    LibraryIndex index,
    CoreTypes coreTypes,
    ClassHierarchy hierarchy,
    this.diagnosticReporter,
    this.referenceFromIndex,
  ) : assetClass = index.getClass('dart:ffi', 'DefaultAsset'),
      nativeClass = index.getClass('dart:ffi', 'Native'),
      nativeFunctionClass = index.getClass('dart:ffi', 'NativeFunction'),
      assetAssetField = index.getField('dart:ffi', 'DefaultAsset', 'id'),
      nativeSymbolField = index.getField('dart:ffi', 'Native', 'symbol'),
      nativeAssetField = index.getField('dart:ffi', 'Native', 'assetId'),
      resolverField = index.getField('dart:ffi', 'Native', '_ffi_resolver'),
      super(
        index,
        coreTypes,
        hierarchy,
        diagnosticReporter,
        referenceFromIndex,
      );

  @override
  TreeNode visitLibrary(Library node) {
    assert(currentAsset == null);
    final annotation = tryGetAssetAnnotationOrWarnOnDuplicates(node);
    if (annotation != null) {
      currentAsset =
          (annotation.constant as InstanceConstant).fieldValues[assetAssetField
                  .fieldReference]
              as StringConstant;
    }
    final result = super.visitLibrary(node);
    currentAsset = null;
    return result;
  }

  List<ConstantExpression> tryGetAnnotation(
    Annotatable node,
    Class instanceOf,
  ) {
    final annotations = <ConstantExpression>[];

    for (final Expression annotation in node.annotations) {
      if (annotation is! ConstantExpression) {
        continue;
      }
      final annotationConstant = annotation.constant;
      if (annotationConstant is! InstanceConstant) {
        continue;
      }
      if (annotationConstant.classNode == instanceOf) {
        annotations.add(annotation);
      }
    }

    return annotations;
  }

  ConstantExpression? tryGetAssetAnnotationOrWarnOnDuplicates(Library node) {
    ConstantExpression? assetAnnotation;

    for (final annotation in tryGetAnnotation(node, assetClass)) {
      if (assetAnnotation != null) {
        // Duplicate @DefaultAsset annotations are forbidden.
        diagnosticReporter.report(
          messageFfiDefaultAssetDuplicate,
          annotation.fileOffset,
          1,
          annotation.location?.file,
        );
        return null;
      } else {
        assetAnnotation = annotation;
      }
    }

    return assetAnnotation;
  }

  ConstantExpression? tryGetNativeAnnotationOrWarnOnDuplicates(Member node) {
    ConstantExpression? nativeAnnotation;

    for (final annotation in tryGetAnnotation(node, nativeClass)) {
      if (nativeAnnotation != null) {
        // Duplicate @Native annotations are forbidden.
        diagnosticReporter.report(
          messageFfiNativeDuplicateAnnotations,
          node.fileOffset,
          1,
          node.location?.file,
        );
        return null;
      } else {
        nativeAnnotation = annotation;
      }
    }

    return nativeAnnotation;
  }

  bool _extendsNativeFieldWrapperClass1(DartType type) {
    if (type is InterfaceType) {
      Class? cls = type.classNode;
      while (cls != null) {
        if (cls == nativeFieldWrapperClass1Class) {
          return true;
        }
        cls = cls.superclass;
      }
    }
    return false;
  }

  StringConstant _resolveNativeSymbolName(
    Member member,
    InstanceConstant native,
  ) {
    final nativeFunctionConst =
        native.fieldValues[nativeSymbolField.fieldReference];
    return nativeFunctionConst is StringConstant
        ? nativeFunctionConst
        : StringConstant(member.name.text);
  }

  StringConstant? _assetNameFromAnnotation(InstanceConstant native) {
    final assetConstant = native.fieldValues[nativeAssetField.fieldReference];
    return assetConstant is StringConstant ? assetConstant : null;
  }

  bool _isLeaf(InstanceConstant native) {
    return (native.fieldValues[nativeIsLeafField.fieldReference]
            as BoolConstant)
        .value;
  }

  // Replaces parameters with Pointer if:
  // 1) they extend NativeFieldWrapperClass1, and
  // 2) the corresponding FFI parameter is Pointer.
  DartType _wrapArgumentType(
    DartType dartParameterType,
    DartType ffiParameterType,
  ) {
    if (dartParameterType is InterfaceType) {
      if (_extendsNativeFieldWrapperClass1(dartParameterType) &&
          env.isSubtypeOf(
            ffiParameterType,
            pointerVoidType,
            SubtypeCheckMode.ignoringNullabilities,
          )) {
        return pointerVoidType;
      }
    }
    return dartParameterType;
  }

  // Replaces return type with Object if it is Handle.
  DartType _wrapReturnType(DartType dartReturnType, DartType ffiReturnType) {
    if (env.isSubtypeOf(
          ffiReturnType,
          handleClass.getThisType(coreTypes, Nullability.nonNullable),
          SubtypeCheckMode.ignoringNullabilities,
        ) &&
        dartReturnType is! VoidType) {
      return objectClass.getThisType(coreTypes, dartReturnType.nullability);
    }
    return dartReturnType;
  }

  // Compute synthetic FFI function type, accounting for Objects passed as
  // Pointer, and Objects returned as Handles.
  FunctionType _wrapFunctionType(
    FunctionType dartFunctionType,
    FunctionType ffiFunctionType,
  ) {
    return FunctionType(
      <DartType>[
        for (var i = 0; i < dartFunctionType.positionalParameters.length; i++)
          _wrapArgumentType(
            dartFunctionType.positionalParameters[i],
            ffiFunctionType.positionalParameters[i],
          ),
      ],
      _wrapReturnType(dartFunctionType.returnType, ffiFunctionType.returnType),
      dartFunctionType.nullability,
    );
  }

  // Whether a parameter of [dartParameterType], passed as [ffiParameterType],
  // needs to be converted to Pointer.
  bool _requiresPointerConversion(
    DartType dartParameterType,
    DartType ffiParameterType,
  ) {
    return (env.isSubtypeOf(
          ffiParameterType,
          pointerVoidType,
          SubtypeCheckMode.ignoringNullabilities,
        ) &&
        !env.isSubtypeOf(
          dartParameterType,
          pointerVoidType,
          SubtypeCheckMode.ignoringNullabilities,
        ));
  }

  VariableDeclaration _declareTemporary(
    Expression initializer,
    DartType dartParameterType,
    DartType ffiParameterType,
  ) {
    final wrappedType =
        (_requiresPointerConversion(dartParameterType, ffiParameterType)
            ? nativeFieldWrapperClass1Type
            : dartParameterType);
    return VariableDeclaration(
      variableDeclarationTemporaryName,
      initializer: initializer,
      type: wrappedType,
      isFinal: true,
      isSynthesized: true,
    );
  }

  Expression _getTemporary(
    VariableDeclaration temporary,
    DartType dartParameterType,
    DartType ffiParameterType, {
    required bool checkForNullptr,
  }) {
    if (_requiresPointerConversion(dartParameterType, ffiParameterType)) {
      Expression pointerAddress = StaticInvocation(
        getNativeFieldFunction,
        Arguments(<Expression>[VariableGet(temporary)]),
      );

      if (checkForNullptr) {
        final pointerAddressVar = VariableDeclaration(
          "#pointerAddress",
          initializer: pointerAddress,
          type: coreTypes.intNonNullableRawType,
          isSynthesized: true,
        );
        pointerAddress = BlockExpression(
          Block([
            pointerAddressVar,
            IfStatement(
              InstanceInvocation(
                InstanceAccessKind.Instance,
                VariableGet(pointerAddressVar),
                objectEquals.name,
                Arguments([ConstantExpression(IntConstant(0))]),
                interfaceTarget: objectEquals,
                functionType: objectEquals.getterType as FunctionType,
              ),
              ExpressionStatement(
                StaticInvocation(
                  stateErrorThrowNewFunction,
                  Arguments([
                    ConstantExpression(
                      StringConstant(
                        'A Dart object attempted to access a native peer, '
                        'but the native peer has been collected (nullptr). '
                        'This is usually the result of calling methods on a '
                        'native-backed object when the native resources have '
                        'already been disposed.',
                      ),
                    ),
                  ]),
                ),
              ),
              EmptyStatement(),
            ),
          ]),
          VariableGet(pointerAddressVar),
        );
      }

      return StaticInvocation(
        fromAddressInternal,
        Arguments(<Expression>[pointerAddress], types: <DartType>[voidType]),
      );
    }
    return VariableGet(temporary);
  }

  // Native calls that pass objects extending NativeFieldWrapperClass1
  // should be passed as Pointer instead so we don't have the overhead of
  // converting Handles.
  // If we find a NativeFieldWrapperClass1 object being passed to an Native
  // signature taking a Pointer, we automatically wrap the argument in a call to
  // `Pointer.fromAddress(_getNativeField(obj))`.
  //
  // Example:
  //   passAsPointer(ClassWithNativeField());
  //
  // Becomes, roughly:
  //   {
  //     final NativeFieldWrapperClass1#t0 = ClassWithNativeField();
  //     final #t1 = passAsPointer(Pointer.fromAddress(_getNativeField(#t0)));
  //     reachabilityFence(#t0);
  //   } => #t1
  Expression _wrapArgumentsAndReturn({
    required StaticInvocation invocation,
    required FunctionType dartFunctionType,
    required FunctionType ffiFunctionType,
    bool checkReceiverForNullptr = false,
  }) {
    List<DartType> ffiParameters = ffiFunctionType.positionalParameters;
    List<DartType> dartParameters = dartFunctionType.positionalParameters;
    // Create lists of temporary variables for arguments potentially being
    // wrapped, and the (potentially) wrapped arguments to be passed.
    final temporariesForArguments = [];
    final callArguments = <Expression>[];
    final fencedArguments = [];
    for (int i = 0; i < invocation.arguments.positional.length; i++) {
      final temporary = _declareTemporary(
        invocation.arguments.positional[i],
        dartParameters[i],
        ffiParameters[i],
      );
      // Note: We also evaluate, and assign temporaries for, non-wrapped
      // arguments as we need to preserve the original evaluation order.
      temporariesForArguments.add(temporary);
      callArguments.add(
        _getTemporary(
          temporary,
          dartParameters[i],
          ffiParameters[i],
          checkForNullptr: checkReceiverForNullptr && i == 0,
        ),
      );
      if (_requiresPointerConversion(dartParameters[i], ffiParameters[i])) {
        fencedArguments.add(temporary);
        continue;
      }
    }

    Expression resultInitializer = invocation;
    if (env.isSubtypeOf(
      ffiFunctionType.returnType,
      handleClass.getThisType(coreTypes, Nullability.nonNullable),
      SubtypeCheckMode.ignoringNullabilities,
    )) {
      resultInitializer = StaticInvocation(
        unsafeCastMethod,
        Arguments([invocation], types: [dartFunctionType.returnType]),
      );
    }

    //   final T #t1 = foo(Pointer.fromAddress(_getNativeField(#t0)));
    final result = VariableDeclaration(
      variableDeclarationTemporaryName,
      initializer: resultInitializer,
      type: dartFunctionType.returnType,
      isFinal: true,
      isSynthesized: true,
    );

    invocation.arguments = Arguments(callArguments);

    // {
    //   final NativeFieldWrapperClass1 #t0 = ClassWithNativeField();
    //   .. #t1 ..
    //   reachabilityFence(#t0);
    // } => #t1
    final resultBlock = BlockExpression(
      Block(<Statement>[
        ...temporariesForArguments,
        result,
        for (final argument in fencedArguments)
          ExpressionStatement(
            StaticInvocation(
              reachabilityFenceFunction,
              Arguments(<Expression>[VariableGet(argument)]),
            ),
          ),
      ]),
      VariableGet(result),
    );

    return resultBlock;
  }

  // Verify the Dart and FFI parameter types are compatible.
  bool _verifyParameter(
    DartType dartParameterType,
    DartType ffiParameterType,
    int annotationOffset,
    Uri? file,
  ) {
    // Only NativeFieldWrapperClass1 instances can be passed as pointer.
    if (_requiresPointerConversion(dartParameterType, ffiParameterType) &&
        !_extendsNativeFieldWrapperClass1(dartParameterType)) {
      diagnosticReporter.report(
        messageFfiNativeOnlyNativeFieldWrapperClassCanBePointer,
        annotationOffset,
        1,
        file,
      );
      return false;
    }
    return true;
  }

  // Verify the signatures of the Dart function and the accompanying FfiNative
  // annotation matches.
  bool _verifySignatures(
    Procedure node,
    FunctionType dartFunctionType,
    FunctionType ffiFunctionType,
    int annotationOffset,
  ) {
    if (ffiFunctionType.namedParameters.isNotEmpty) {
      diagnosticReporter.report(
        templateCantHaveNamedParameters.withArguments('FfiNative'),
        annotationOffset,
        0,
        node.location?.file,
      );
      return false;
    }

    if (ffiFunctionType.positionalParameters.length >
        ffiFunctionType.requiredParameterCount) {
      diagnosticReporter.report(
        templateCantHaveOptionalParameters.withArguments('FfiNative'),
        annotationOffset,
        0,
        node.location?.file,
      );
      return false;
    }

    if (dartFunctionType.positionalParameters.length !=
        ffiFunctionType.positionalParameters.length) {
      final template =
          (node.isStatic
              ? templateFfiNativeUnexpectedNumberOfParameters
              : templateFfiNativeUnexpectedNumberOfParametersWithReceiver);
      diagnosticReporter.report(
        template.withArguments(
          dartFunctionType.positionalParameters.length,
          ffiFunctionType.positionalParameters.length,
        ),
        annotationOffset,
        1,
        node.location?.file,
      );
      return false;
    }

    var validSignature = true;
    for (var i = 0; i < dartFunctionType.positionalParameters.length; i++) {
      final dartParameterType = dartFunctionType.positionalParameters[i];
      if (dartParameterType is InterfaceType) {
        if (!_verifyParameter(
          dartParameterType,
          ffiFunctionType.positionalParameters[i],
          annotationOffset,
          node.location?.file,
        )) {
          validSignature = false;
        }
      }
    }

    return validSignature;
  }

  static const _vmFfiNative = 'vm:ffi:native';

  /// A pragma annotation added to methods previously annotated with `@Native`
  /// during the transformation so that the use site transformer can find valid
  /// targets.
  static const nativeMarker = 'cfe:ffi:native-marker';

  Procedure _transformProcedure(
    Procedure node,
    StringConstant nativeFunctionName,
    StringConstant? overriddenAssetName,
    bool isLeaf,
    int annotationOffset,
    FunctionType dartFunctionType,
    FunctionType ffiFunctionType,
    List<Expression> argumentList, {
    required bool checkReceiverForNullptr,
  }) {
    final wrappedDartFunctionType = checkFfiType(
      node,
      dartFunctionType,
      ffiFunctionType,
      isLeaf,
      annotationOffset,
    );

    if (wrappedDartFunctionType == null) {
      // It's OK to continue because the diagnostics issued will cause
      // compilation to fail. By continuing, we can report more diagnostics
      // before compilation ends.
      return node;
    }

    final resolvedNative = _generateResolvedNativeConstant(
      nativeType: ffiFunctionType,
      nativeName: nativeFunctionName,
      isLeaf: isLeaf,
      overriddenAssetName: overriddenAssetName,
    );
    final pragmaConstant = ConstantExpression(
      InstanceConstant(pragmaClass.reference, [], {
        pragmaName.fieldReference: StringConstant(_vmFfiNative),
        pragmaOptions.fieldReference: resolvedNative,
      }),
      InterfaceType(pragmaClass, Nullability.nonNullable, []),
    );

    // The marker annotation is always added to the original method so that the
    // use-sites transformer can find it when a tear-off is passed to e.g.
    // Native.addressOf.
    node.addAnnotation(
      ConstantExpression(
        InstanceConstant(pragmaClass.reference, [], {
          pragmaName.fieldReference: StringConstant(nativeMarker),
          pragmaOptions.fieldReference: resolvedNative,
        }),
        InterfaceType(pragmaClass, Nullability.nonNullable, []),
      ),
    );

    final possibleCompoundReturn = findCompoundReturnType(dartFunctionType);
    if (dartFunctionType == wrappedDartFunctionType &&
        node.isStatic &&
        possibleCompoundReturn == null) {
      // We are not wrapping/unwrapping arguments or return value.
      node.addAnnotation(pragmaConstant);
      return node;
    }

    // Introduce a new function as external with the annotation and give the
    // current function a body that does the wrapping/unwrapping.
    node.isExternal = false;

    addPragmaPreferInline(node);

    final parent = node.parent;
    final fileUri = node.fileUri;

    final name = Name(
      '_${node.name.text}\$${node.kind.name}\$FfiNative',
      currentLibrary,
    );
    final reference = currentLibraryIndex?.lookupGetterReference(name);

    int varCounter = 0;
    final nonWrappedFfiNative = Procedure(
      name,
      ProcedureKind.Method,
      FunctionNode(
        /*body=*/ null,
        requiredParameterCount: wrappedDartFunctionType.requiredParameterCount,
        positionalParameters: [
          for (final positionalParameter
              in wrappedDartFunctionType.positionalParameters)
            VariableDeclaration(
              /*name=*/ '#t${varCounter++}',
              type: positionalParameter,
            )..fileOffset = node.fileOffset,
        ],
        returnType: wrappedDartFunctionType.returnType,
      )..fileOffset = node.fileOffset,
      fileUri: fileUri,
      isStatic: true,
      isExternal: true,
      reference: reference,
    )..fileOffset = node.fileOffset;
    nonWrappedFfiNative.addAnnotation(pragmaConstant);

    // Add procedure to the parent the FfiNative function belongs to.
    if (parent is Class) {
      parent.addProcedure(nonWrappedFfiNative);
    } else if (parent is Library) {
      parent.addProcedure(nonWrappedFfiNative);
    } else {
      throw 'Unexpected parent of @Native function. '
          'Expected Class or Library, but found ${parent}.';
    }

    final nonWrappedInvocation = StaticInvocation(
      nonWrappedFfiNative,
      Arguments(argumentList),
    )..fileOffset = node.fileOffset;

    Expression result =
        (wrappedDartFunctionType == dartFunctionType
            ? nonWrappedInvocation
            : _wrapArgumentsAndReturn(
              invocation: nonWrappedInvocation,
              dartFunctionType: dartFunctionType,
              ffiFunctionType: ffiFunctionType,
              checkReceiverForNullptr: checkReceiverForNullptr,
            ));
    if (possibleCompoundReturn != null) {
      result = invokeCompoundConstructor(result, possibleCompoundReturn);
    }

    node.function.body = ReturnStatement(result)..parent = node.function;

    return node;
  }

  /// Creates a `Native` constant with all fields resolved.
  ///
  /// Re-using the constant from the original `@Native` annotation doesn't work
  /// because the asset field may be inferred from the library.
  InstanceConstant _generateResolvedNativeConstant({
    required DartType nativeType,
    required StringConstant nativeName,
    required bool isLeaf,
    StringConstant? overriddenAssetName,
  }) {
    return InstanceConstant(
      nativeClass.reference,
      [nativeType],
      {
        nativeSymbolField.fieldReference: nativeName,
        nativeAssetField.fieldReference:
            overriddenAssetName ??
            currentAsset ??
            StringConstant(currentLibrary.importUri.toString()),
        nativeIsLeafField.fieldReference: BoolConstant(isLeaf),
      },
    );
  }

  // Transform Native instance methods.
  //
  // Example:
  //
  //   class MyNativeClass extends NativeFieldWrapperClass1 {
  //     @Native<IntPtr Function(Pointer<Void>, IntPtr)>(symbol: 'MyClass_MyMethod')
  //     external int myMethod(int x);
  //   }
  //
  // Becomes, roughly:
  //
  //   ... {
  //     @pragma(
  //       'vm:ffi:native',
  //       Native<IntPtr Function(Pointer<Void>, IntPtr)>(
  //         symbol: 'MyClass_MyMethod',
  //         assetId: '<library uri>',
  //       ),
  //     )
  //     external int _myFunction$FfiNative(Pointer<Void> self, int x);
  //
  //     @pragma('vm:prefer-inline')
  //     int myMethod(int x) => _myMethod$FfiNative(_getNativeField(this), x);
  //   }
  Procedure _transformInstanceMethod(
    Procedure node,
    FunctionType ffiFunctionType,
    StringConstant nativeFunctionName,
    StringConstant? assetName,
    bool isLeaf,
    int annotationOffset,
  ) {
    final dartFunctionType = FunctionType(
      [
        (node.parent as Class).getThisType(coreTypes, Nullability.nonNullable),
        for (final parameter in node.function.positionalParameters)
          parameter.type,
      ],
      node.function.returnType,
      Nullability.nonNullable,
    );

    final argumentList = <Expression>[
      ThisExpression(),
      for (final parameter in node.function.positionalParameters)
        VariableGet(parameter),
    ];

    return _transformProcedure(
      node,
      nativeFunctionName,
      assetName,
      isLeaf,
      annotationOffset,
      dartFunctionType,
      ffiFunctionType,
      argumentList,
      checkReceiverForNullptr: true,
    );
  }

  // Transform Native static functions.
  //
  // Example:
  //
  //   @Native<IntPtr Function(Pointer<Void>, IntPtr)>(symbol: 'MyFunction')
  //   external int myFunction(MyNativeClass obj, int x);
  //
  // Becomes, roughly:
  //
  //   @pragma(
  //     'vm:ffi:native',
  //     Native<IntPtr Function(Pointer<Void>, IntPtr)>(
  //       symbol: 'MyFunction',
  //       assetId: '<library uri>',
  //     ),
  //   )
  //   external int _myFunction$FfiNative(Pointer<Void> self, int x);
  //
  //   @pragma('vm:prefer-inline')
  //   int myFunction(MyNativeClass obj, int x)
  //     => _myFunction$FfiNative(
  //       Pointer<Void>.fromAddress(_getNativeField(obj)), x);
  Procedure _transformStaticFunction(
    Procedure node,
    FunctionType ffiFunctionType,
    StringConstant nativeFunctionName,
    StringConstant? overriddenAssetName,
    bool isLeaf,
    int annotationOffset,
  ) {
    final dartFunctionType = node.function.computeThisFunctionType(
      Nullability.nonNullable,
    );

    final argumentList = <Expression>[
      for (final parameter in node.function.positionalParameters)
        VariableGet(parameter),
    ];

    return _transformProcedure(
      node,
      nativeFunctionName,
      overriddenAssetName,
      isLeaf,
      annotationOffset,
      dartFunctionType,
      ffiFunctionType,
      argumentList,
      checkReceiverForNullptr: false,
    );
  }

  Expression _generateAddressOfField(
    Member node,
    InterfaceType ffiType,
    InstanceConstant native,
  ) {
    return StaticInvocation(
      nativePrivateAddressOf,
      Arguments([ConstantExpression(native)], types: [ffiType]),
    )..fileOffset = node.fileOffset;
  }

  (DartType, NativeTypeCfe) _validateOrInferNativeFieldType(
    Member node,
    DartType ffiType,
    DartType dartType,
  ) {
    if (ffiType is DynamicType) {
      // If no type argument is given on the @Native annotation, try to infer
      // it.
      final inferred = convertDartTypeToNativeType(dartType);
      if (inferred != null) {
        ffiType = inferred;
      } else {
        diagnosticReporter.report(
          messageFfiNativeFieldMissingType,
          node.fileOffset,
          1,
          node.location?.file,
        );
        throw FfiStaticTypeError();
      }
    }

    ensureNativeTypeValid(
      ffiType,
      node,
      allowStructAndUnion: true,
      // Handles are not currently supported, but checking them separately and
      // allowing them here yields a more specific error message.
      allowHandle: true,
      allowInlineArray: true,
    );
    ensureNativeTypeMatch(
      FfiTypeCheckDirection.nativeToDart,
      ffiType,
      dartType,
      node,
      allowHandle: true,
      allowArray: true,
    );

    // Array types must have an @Array annotation denoting its size.
    if (isArrayType(ffiType)) {
      final dimensions = ensureArraySizeAnnotation(node, ffiType, false);
      return (
        ffiType,
        NativeTypeCfe.withoutLayout(
          this,
          dartType,
          arrayDimensions: dimensions,
          variableLength: false,
        ),
      );
    }

    // Apart from arrays, compound subtypes, pointers and numeric types are
    // supported for fields.
    if (!isStructOrUnionSubtype(ffiType) &&
        !isAbiSpecificIntegerSubtype(ffiType)) {
      final type = switch (ffiType) {
        InterfaceType(:var classNode) => getType(classNode),
        _ => null,
      };
      if (type == null ||
          type == NativeType.kNativeFunction ||
          type == NativeType.kHandle ||
          isArrayType(ffiType)) {
        diagnosticReporter.report(
          messageFfiNativeFieldType,
          node.fileOffset,
          1,
          node.location?.file,
        );
        throw FfiStaticTypeError();
      }
    }

    return (ffiType, NativeTypeCfe.withoutLayout(this, ffiType));
  }

  DartType _validateOrInferNativeFunctionType(
    Procedure node,
    DartType nativeType,
    FunctionType dartType,
  ) {
    if (nativeType is DynamicType) {
      // No type argument was given on the @Native annotation, so we try to
      // infer the native type from the Dart signature.
      final inferred = convertDartTypeToNativeType(dartType, allowVoid: true);
      if (inferred != null) {
        nativeType = inferred;
      }

      final nativeFunctionType = InterfaceType(
        nativeFunctionClass,
        Nullability.nonNullable,
        [nativeType],
      );

      if (!isNativeTypeValid(
        nativeFunctionType,
        allowHandle: true,
        allowStructAndUnion: true,
      )) {
        diagnosticReporter.report(
          messageFfiNativeFunctionMissingType,
          node.fileOffset,
          1,
          node.location?.file,
        );
        throw FfiStaticTypeError();
      }
    }

    return nativeType;
  }

  @override
  TreeNode visitField(Field node) {
    final nativeAnnotation = tryGetNativeAnnotationOrWarnOnDuplicates(node);
    if (nativeAnnotation == null) {
      return node;
    }
    // @Native annotations on fields are valid if the field is external. But
    // external fields are represented as a getter/setter pair in Kernel, so
    // only visit fields to verify that no native annotation is present.
    assert(!node.isExternal);
    diagnosticReporter.report(
      messageFfiNativeMustBeExternal,
      node.fileOffset,
      1,
      node.location?.file,
    );
    return node;
  }

  @override
  visitProcedure(Procedure node) {
    // Only transform functions that are external and have Native annotation:
    //   @Native<Double Function(Double)>(symbol: 'Math_sqrt')
    //   external double _square_root(double x);
    final ffiNativeAnnotation = tryGetNativeAnnotationOrWarnOnDuplicates(node);
    if (ffiNativeAnnotation == null) {
      return node;
    }

    if (!node.isExternal) {
      diagnosticReporter.report(
        messageFfiNativeMustBeExternal,
        node.fileOffset,
        1,
        node.location?.file,
      );
      return node;
    }

    node.annotations.remove(ffiNativeAnnotation);

    final ffiConstant = ffiNativeAnnotation.constant as InstanceConstant;
    var nativeType = ffiConstant.typeArguments[0];

    if (node.kind == ProcedureKind.Method) {
      final dartType = node.function.computeFunctionType(
        Nullability.nonNullable,
      );
      try {
        nativeType = _validateOrInferNativeFunctionType(
          node,
          nativeType,
          dartType,
        );
      } on FfiStaticTypeError {
        // We've already reported an error.
        return node;
      }
    }

    final nativeName = _resolveNativeSymbolName(node, ffiConstant);
    final overriddenAssetName = _assetNameFromAnnotation(ffiConstant);
    final isLeaf = _isLeaf(ffiConstant);

    if (nativeType is FunctionType) {
      try {
        final nativeFunctionType = InterfaceType(
          nativeFunctionClass,
          Nullability.nonNullable,
          [nativeType],
        );
        ensureNativeTypeValid(
          nativeFunctionType,
          ffiNativeAnnotation,
          allowStructAndUnion: true,
          allowHandle: true,
        );
      } on FfiStaticTypeError {
        // We've already reported an error.
        return node;
      }

      if (!node.isStatic) {
        return _transformInstanceMethod(
          node,
          nativeType,
          nativeName,
          overriddenAssetName,
          isLeaf,
          ffiNativeAnnotation.fileOffset,
        );
      }

      return _transformStaticFunction(
        node,
        nativeType,
        nativeName,
        overriddenAssetName,
        isLeaf,
        ffiNativeAnnotation.fileOffset,
      );
    } else if (node.kind == ProcedureKind.Getter ||
        node.kind == ProcedureKind.Setter) {
      if (!node.isStatic) {
        diagnosticReporter.report(
          messageFfiNativeFieldMustBeStatic,
          node.fileOffset,
          1,
          node.location?.file,
        );
      }

      DartType dartType;
      NativeTypeCfe nativeTypeCfe;
      try {
        dartType =
            node.kind == ProcedureKind.Getter
                ? node.function.returnType
                : node.function.positionalParameters[0].type;
        (nativeType, nativeTypeCfe) = _validateOrInferNativeFieldType(
          node,
          nativeType,
          dartType,
        );
      } on FfiStaticTypeError {
        return node;
      }
      final resolved = _generateResolvedNativeConstant(
        nativeType: nativeType,
        nativeName: nativeName,
        isLeaf: isLeaf,
        overriddenAssetName: overriddenAssetName,
      );
      node.isExternal = false;

      final zeroOffset = ConstantExpression(IntConstant(0));

      if (node.kind == ProcedureKind.Getter) {
        node.function.body = ReturnStatement(
          nativeTypeCfe.generateLoad(
            dartType: dartType,
            fileOffset: node.fileOffset,
            typedDataBase: _generateAddressOfField(
              node,
              nativeType as InterfaceType,
              resolved,
            ),
            transformer: this,
            offsetInBytes: zeroOffset,
          ),
        );
        node.annotations.add(
          ConstantExpression(
            InstanceConstant(pragmaClass.reference, [], {
              pragmaName.fieldReference: StringConstant(nativeMarker),
              pragmaOptions.fieldReference: resolved,
            }),
            InterfaceType(pragmaClass, Nullability.nonNullable, []),
          ),
        );
      } else {
        node.function.body = ExpressionStatement(
          nativeTypeCfe.generateStore(
            node.function.positionalParameters[0],
            dartType: dartType,
            fileOffset: node.fileOffset,
            typedDataBase: _generateAddressOfField(
              node,
              nativeType as InterfaceType,
              resolved,
            ),
            transformer: this,
            offsetInBytes: zeroOffset,
          ),
        );
      }
    } else {
      // This function is not annotated with a native function type, which is
      // invalid.
      diagnosticReporter.report(
        templateFfiTypeInvalid.withArguments(nativeType),
        node.fileOffset,
        1,
        node.location?.file,
      );
    }

    return node;
  }

  /// Checks whether the FFI function type is valid and reports any errors.
  /// Returns the Dart function type for the FFI function if the type is valid.
  ///
  /// For example, for FFI function type `Int8 Function(Double)`, this returns
  /// `int Function(double)`.
  FunctionType? checkFfiType(
    Procedure node,
    FunctionType dartFunctionType,
    FunctionType ffiFunctionTypeWithPossibleVarArgs,
    bool isLeaf,
    int annotationOffset,
  ) {
    final ffiFunctionType = flattenVarargs(ffiFunctionTypeWithPossibleVarArgs);
    if (!_verifySignatures(
      node,
      dartFunctionType,
      ffiFunctionType,
      annotationOffset,
    )) {
      return null;
    }

    // int Function(Pointer<Void>)
    final wrappedDartFunctionType = _wrapFunctionType(
      dartFunctionType,
      ffiFunctionType,
    );

    final nativeType = InterfaceType(
      nativeFunctionClass,
      Nullability.nonNullable,
      [ffiFunctionType],
    );

    try {
      ensureNativeTypeValid(nativeType, node);
      ensureNativeTypeMatch(
        FfiTypeCheckDirection.nativeToDart,
        nativeType,
        wrappedDartFunctionType,
        node,
        allowHandle: true, // Handle-specific errors emitted below.
      );
      ensureLeafCallDoesNotUseHandles(nativeType, isLeaf, reportErrorOn: node);
    } on FfiStaticTypeError {
      // It's OK to swallow the exception because the diagnostics issued will
      // cause compilation to fail. By continuing, we can report more
      // diagnostics before compilation ends.
      return null;
    }

    return wrappedDartFunctionType;
  }
}
